﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Diagnostics;

namespace Yoshizawa
{
    /// <summary>
    /// 
    /// </summary>
    public class TinyMapper
    {
        private readonly IDbConnection connection;
        private IDbTransaction transaction;
        private static readonly object syncObj = new object();
        private readonly object transactionSync = new object();
        private static readonly Dictionary<Type, List<PropertyInfo>> propertyInfoDic = new Dictionary<Type, List<PropertyInfo>>();

        private static readonly Dictionary<Type, object> defaultTypeValues = new Dictionary<Type, object>()
        {
            {
                typeof(string),
                string.Empty
            }
            ,
            {
                typeof(int),
                0
            }
            ,
            {
                typeof(long),
                0
            },
            {
                typeof(DateTime),
                DateTime.MaxValue
            }
        };

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="connection">DBコネクション</param>
        public TinyMapper(IDbConnection connection)
        {
            if (null == connection)
            {
                throw new ArgumentNullException("connection");
            }

            this.connection = connection;
        }

        /// <summary>
        /// SQLコマンド生成
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        private IDbCommand CreateCommand(string sql, params object[] parameters)
        {
            if (string.IsNullOrEmpty(sql))
            {
                throw new ArgumentNullException("sql");
            }

            IDbCommand cmd = this.connection.CreateCommand();
            cmd.CommandText = sql;

            if (null != this.transaction)
            {
                cmd.Transaction = this.transaction;
            }

            for (int i = 0; i < parameters.Length; i += 2)
            {
                IDbDataParameter p = cmd.CreateParameter();
                p.ParameterName = (string)parameters[i];
                p.Value = parameters[i + 1];
                cmd.Parameters.Add(p);
            }

            return cmd;
        }

        /// <summary>
        /// SQLトランザクション
        /// </summary>
        /// <param name="action"></param>
        public void Transaction(Action action)
        {
            if (null == action)
            {
                throw new ArgumentNullException("action");
            }

            lock (this.transactionSync)
            {
                if (null != this.transaction)
                {
                    throw new InvalidProgramException("Already Transaction.");
                }

                using (this.transaction = this.connection.BeginTransaction())
                {
                    try
                    {
                        action();
                        this.transaction.Commit();
                    }
                    catch
                    {
                        this.transaction.Rollback();
                        throw;
                    }
                    finally
                    {
                        this.transaction = null;
                    }
                }
            }
        }

        /// <summary>
        /// SQL実行
        /// </summary>
        /// <param name="sql"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public int Execute(string sql, params object[] parameters)
        {
            if (string.IsNullOrEmpty(sql))
            {
                throw new ArgumentNullException("sql");
            }

            try
            {
                using (IDbCommand cmd = this.CreateCommand(sql, parameters))
                {
                    return cmd.ExecuteNonQuery();
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.ToString());
                Trace.TraceError(sql);
                throw;
            }
        }

        /// <summary>
        /// SQL実行
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sql"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public T ExecuteScalar<T>(string sql, params object[] parameters)
        {
            if (string.IsNullOrEmpty(sql))
            {
                throw new ArgumentNullException("sql");
            }

            try
            {
                using (IDbCommand cmd = this.CreateCommand(sql, parameters))
                {
                    object result = cmd.ExecuteScalar();

                    if (Convert.IsDBNull(result) || null == result)
                    {
                        return default(T);
                    }

                    return (T)result;
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.ToString());
                Trace.TraceError(sql);
                throw;
            }
        }

        public List<T> ExecuteReader<T>(string sql, params object[] parameters) where T : new()
        {
            if (string.IsNullOrEmpty(sql))
            {
                throw new ArgumentNullException("sql");
            }

            List<T> results = new List<T>();

            try
            {
                using (IDbCommand cmd = this.CreateCommand(sql, parameters))
                {
                    using (IDataReader reader = cmd.ExecuteReader())
                    {
                        var fields = new HashSet<string>();

                        for (int i = 0; i < reader.FieldCount; i++)
                        {
                            fields.Add(reader.GetName(i));
                        }

                        while (reader.Read())
                        {
                            T obj = this.ReadValues<T>(reader, fields);
                            results.Add(obj);
                        }

                        return results;
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.ToString());
                Trace.TraceError(sql);
                throw;
            }
        }

        /// <summary>
        /// DataReaderから値を取得
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="reader"></param>
        /// <returns></returns>
        private T ReadValues<T>(IDataReader reader, IEnumerable<string> fields) where T : new()
        {
            T obj = new T();
            var type = obj.GetType();

            lock (syncObj)
            {
                if (!propertyInfoDic.ContainsKey(type))
                {
                    propertyInfoDic.Add(type, type.GetProperties().Where(i => i.CanWrite).ToList());
                }
            }

            propertyInfoDic[type].ForEach(
                p =>
                {
                    if (!fields.Contains(p.Name))
                    {
                        return;
                    }

                    object value = reader[p.Name];

                    if (Convert.IsDBNull(value))
                    {
                        if (defaultTypeValues.ContainsKey(p.PropertyType))
                        {
                            value = defaultTypeValues[p.PropertyType];
                        }
                        else
                        {
                            value = Activator.CreateInstance(p.PropertyType);
                        }
                    }

                    // SQLite(x64)における整数型の変換
                    if (typeof(Int64).IsInstanceOfType(value) && typeof(Int32).FullName == p.PropertyType.FullName)
                    {
                        value = Convert.ToInt32(value);
                    }

                    p.SetValue(obj, value, null);
                });

            return obj;
        }

    }
}
